<?php

namespace Desktopd\PlatformTools;

//use Desktopd\PlatformTools\Log; // TODO: use non-global instantiated logger


class DaemonTools {
    protected $name;
    protected $errorHandler;
    protected $properties = array();
    protected $exitHandlers = array();
    protected $signals = array();
    protected $restart = false;
    protected $startTime;
    protected $count = 0;
    protected $exitCalled = false;
    protected $errorsToExit = 0;
    
    
    public function __construct ($name = '(Desktopd)', $errorsToExit = 0) {
        $this->errorsToExit = (int) $errorsToExit;
        
        $this->startTime = explode(' ', microtime());
        Log::log("Initializing DaemonTools ($name) ...", Log::INFO);
        
        $this->name = "$name";
        
        Log::log("Enabling the error handler...", Log::DEBUG);
        set_error_handler($this->errorHandler = function (
            $no
            , $str
            , $file
            , $line
            , array $context
            , $fatal = false
        ) {
            $errorConstants = array(
                'E_ERROR' => 'PHP Fatal error',
                'E_WARNING' => 'PHP Warning',
                'E_PARSE' => 'PHP Parse error',
                'E_NOTICE' => 'PHP Notice',
                'E_CORE_ERROR' => 'PHP Core error',
                'E_CORE_WARNING' => 'PHP Core warning',
                'E_COMPILE_ERROR' => 'PHP Compile error',
                'E_COMPILE_WARNING' => 'PHP Compile warning',
                'E_USER_ERROR' => 'PHP Fatal error',
                'E_USER_WARNING' => 'PHP Warning',
                'E_USER_NOTICE' => 'PHP Notice',
                'E_STRICT' => 'PHP Strict Standards',
                'E_RECOVERABLE_ERROR' => 'PHP Catchable fatal error',
                'E_DEPRECATED' => 'PHP Deprecated',
                'E_USER_DEPRECATED' => 'PHP Deprecated',
            );
            
            $type = 'PHP Unknown error';
            
            foreach ($errorConstants as $constant => $name) {
                if (!defined($constant)) continue;
                
                if (constant($constant) == $no) {
                    $type = $name;
                    break;
                }
            }
            
            Log::log("$type: $str in $file on line $line", Log::ERROR);
            
            if ($fatal) {
                // fatal error; on the shutdown function flow (cannot exit())
                Log::log("This error is not recoverable", Log::NOTICE);
            } elseif ($no & $this->errorsToExit) {
                Log::log("Aborting as '$type' is marked as 'fatal'", Log::NOTICE);
                exit(1); // Exit handlers will be called
            } else {
                Log::log("NOTE: Continueing execution as '$type' is NOT marked as 'fatal'!", Log::NOTICE);
            }
        });
        
        ini_set('display_errors', 0);
        ini_set('log_errors', 0);
        
        
        Log::log("Enabling the shutdown function...", Log::DEBUG);
        register_shutdown_function(function () {
            Log::log("Entering the shutdown function!", Log::DEBUG);
            
            if ($error = error_get_last()) {
                $uncatchable = (
                    E_ERROR |
                    E_PARSE |
                    E_CORE_ERROR |
                    E_CORE_WARNING |
                    E_COMPILE_ERROR |
                    E_COMPILE_WARNING
                );
                
                if ($error['type'] & $uncatchable) {
                    $this->errorHandler(
                        $error["type"]
                        , $error["message"]
                        , $error["file"]
                        , $error["line"]
                        , array() // no context
                        , true // Is fatal
                    );
                }
                
            }
            
            
            // cleanups
            $this->callExitHandlers();
            
            return; // clean exit? (DO NOT USE 'exit;' in register_shutdown_function)
        });
        
        
        Log::log("Enabling signal handlers...", Log::DEBUG);
        try {
            if (!function_exists('pcntl_signal')) {
                throw new \RuntimeException('pcntl_signal(): not available');
            }
            
            (new \ReflectionFunction(function ($f, $daemon) {
                $enabled = 0;
                
                foreach (array(
                    // prevent abort
                    'SIGILL',
                    'SIGTRAP',
                    'SIGABRT',
                    'SIGFPE',
                    'SIGBUS',
                    'SIGSERV',
                    'SIGSYS',
                    'SIGXCPU',
                    'SIGXFSZ',
                    'SIGQUIT',
                    
                    // prevent termination
                    'SIGHUP',
                    'SIGINT',
                    'SIGALRM',
                    'SIGTERM',
                    'SIGUSR1',
                    'SIGUSR2',
                    'SIGPOLL',
                    'SIGVTARLM',
                    'SIGPROF',
                    'SIGPIPE',
                ) as $name) {
                    if (defined($name)) {
                        if (pcntl_signal($value = constant($name), $f/*, false*/)) {
                            $daemon->signals[$value] = $name;
                            Log::log("Signal handling enabled: '$name' ($value)", Log::DEBUG2);
                            $enabled++;
                        } else {
                            Log::log("Failed to set a signal handler for '$name'", Log::WARNING);
                        }
                    }
                }
                
                if (!$enabled) {
                    throw new \RuntimeException('Signal handling disabled');
                }
                
            }))->invoke(function ($n) {
                $name = isset($this->signals[$n]) ? "{$this->signals[$n]} ($n)" : "Unregistered signal ($n)";
                
                Log::log("Caught signal: $name", Log::NOTICE);
                $exit = false;
                if (defined('SIGTERM') && $n == constant('SIGTERM')) {
                    $exit = true;
                } elseif (defined('SIGQUIT') && $n == constant('SIGQUIT')) {
                    $exit = true;
                } elseif (defined('SIGABRT') && $n == constant('SIGABRT')) {
                    $exit = true;
                } elseif (defined('SIGINT') && $n == constant('SIGINT')) {
                    $exit = true;
                }
                
                if (defined('SIGHUP') && $n == constant('SIGHUP')) {
                    // daemon restart (SIGHUP)
                    
                    $this->restart = true;
                    $delay = 5;
                    if ($this->restart($delay)) {
                        Log::log("Restarting in $delay s......", Log::NOTICE);
                        $exit = true;
                    } else {
                        Log::log("Failed to restart? (with delay=$delay s)", Log::WARNING);
                    }
                }
                
                if ($exit) {
                    $this->callExitHandlers();
                    exit;
                } else {
                    Log::log("Signal ignored!", Log::NOTICE);
                }
            }, $this);
        } catch (\RuntimeException $e) {
            $message = $e->getMessage();
            Log::log("Failed to set signal handlers; clean exit on signals DISABLED ($message)", Log::WARNING);
        }
        
        
        try {
            if (function_exists('cli_set_process_title')) {
                throw new \RuntimeException('Function cli_set_process_title(): does not exist');
            }
            
            if (!cli_set_process_title($this->name)) {
                throw new \RuntimeException('FAILED: cli_set_process_title()');
            }
            
            Log::log("Process title set to: " . $this->name, Log::NOTICE);
            
        } catch (\Exception $e) {
            $message = $e->getMessage();
            Log::log("Failed to set process title ($message)", Log::NOTICE);
        }
    }
    
    public function addExitHandler ($callback) {
        if (!is_callable($callback)) return false;
        if (!in_array($callback, $this->exitHandlers, true)) {
            $this->exitHandlers[] = $callback;
        }
        
        return count($this->exitHandlers);
    }
    
    public function removeExitHandler ($callback) {
        if (!is_callable($callback)) return false;
        
        foreach (array_keys($this->exitHandlers, $callback, true) as $key) {
            unset($this->exitHandlers[$key]);
        }
        $this->exitHandlers = array_values($this->exitHandlers);
        
        return count($this->exitHandlers);
    }
    
    public function countExitHandlers () {
        return count($this->exitHandlers);
    }
    
    protected function callExitHandlers () {
        if ($this->exitCalled) {
            return;
        }
        
        $this->exitCalled = true;
        
        $count = count($this->exitHandlers);
        Log::log("Calling $count exit handler(s)...", Log::INFO);
        
        // exitHandlers are stacked (push -> pop)
        while ($callback = array_pop($this->exitHandlers)) {
            if (!is_callable($callback)) continue;
            
            $callback($this);
        }
        
        //var_dump($this->properties);
        
        // give destructors a chance to do some cleanups
        // XXX: On shutdown caused by a fatal error, explicit unset() does not cause
        // __destruct() to be called!!!
        // This should be a bug in PHP!
        $properties = &$this->properties;
        
        foreach (array_keys($this->properties) as $key) {
            unset($properties[$key]);
            gc_collect_cycles();
        }
        
        
        Log::log("Trying to exit cleanly......", Log::NOTICE);
    }
    
    public function restart ($delay = null) {
        // not implemented (yet)
        return false;
    }
    
    public function sleepMicro ($ms) {
        if (function_exists('pcntl_signal_dispatch')) {
            pcntl_signal_dispatch();
        }
        
        return usleep($ms);
    }
    
    public function __get ($name) {
        return isset($this->properties[$name]) ? $this->properties[$name] : null;
    }
    
    public function __set ($name, $value) {
        $this->properties[$name] = $value;
    }
    
    public function __isset ($name) {
        return isset($this->properties[$name]);
    }
    
    public function __unset ($name) {
        unset($this->properties[$name]);
    }
    
    public function __destruct () {
        $this->callExitHandlers();
    }
    
    public function __call ($name, array $args = array()) {
        $trace = debug_backtrace();
        
        $callerFile = isset($trace[1]['file']) ? $trace[1]['file'] : null;
        if (__FILE__ !== $callerFile) {
            // public / protected call
            $callable = (new ObjectTools($this))->getProperty($name);
        } else {
            // private call
            $callable = isset($this->{$name}) ? $this->{$name} : null;
        }
        
        if (is_callable($callable)) {
            return (new \ReflectionFunction($callable))->invokeArgs($args);
        } else {
            throw new \RuntimeException("No such callable: $name");
        }
    }
    
    protected function sub_microtime(array $first, array $second) {
        if (function_exists('bcadd')) {
            return bcadd(
                bcsub($second[1], $first[1], 6),
                bcsub($second[0], $first[0], 6),
                6
            );
        } else {
            // minimizing error
            return number_format(
                (
                    ($time[1] - $this->startTime[1]) +
                    ($time[0] - $this->startTime[0])
                ),
                6,
                '.',
                ''
            );
        }
    }
    
    // returns formatted string
    protected function _uptime() {
        if (!is_array($this->startTime) || count($this->startTime) != 2) {
            Log::log("startTime is not correctly set: '" . print_r($this->startTime, true) . "'", Log::WARNING);
            return '__??__';
        }
        
        return $this->sub_microtime(
            $this->startTime,
            explode(' ', microtime())
        );
    }
    
    public function heartbeat() {
        Log::log(sprintf('Heartbeat: uptime=%s, cycle=%d',
            $this->_uptime(),
            $this->count
        ), Log::NOTICE);
        
    }
    
    public function doTasks () {
        $this->count++;
        $this->heartbeat();
        
        // signal handling
        if (function_exists('pcntl_signal_dispatch')) {
            pcntl_signal_dispatch();
        }
        
    }
    
    public function __toString() {
        return sprintf("%s (%s)", __CLASS__, $this->name);
    }
}


// vim: ts=4 et ai

